/*
 * ﻿Copyright (C) 2011-2012 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.aspectjutil.dynamicagent;

import java.io.File;
import java.io.FileFilter;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;

//note: including JavaDoc, intentionally avoid to make direct reference to classes in aspectjweaver.jar in this class.
/**
 * Wrapper class of AspectJ's <code>Agent</code> class, in order to support dynamic loading. <br /> 
 * Though this class is not usually necessary in general because of existence of 
 * <code>WeavingURLClassLoader</code> class in AspectJ, in some special case, this with 
 * <code>AspectJWeaverAgentLoader</code> becomes useful for applying AspectJ's <code>Agent</code> 
 * dynamically. <br />
 * 
 * @see #agentmainWorker(String, Instrumentation)
 * @author <a href="mailto:artymt@gmail.com">Arata Y.</a>
 */
public class DynamicAspectJWeaverAgent {
//TODO create super class and extract general logic and methods to it. 
	static Logger logger = Logger.getLogger( DynamicAspectJWeaverAgent.class.toString());
		static Logger getLogger() {
			return logger;
		}
	
	public static final String AspectjWeaverAgentClassName = "org.aspectj.weaver.loadtime.Agent";
		public static String getAspectjWeaverAgentClassName() {
			return AspectjWeaverAgentClassName;
		}

	private static Instrumentation instrumentation;
		static Instrumentation getInstrumentation() {
			return instrumentation;
		}
		synchronized static void setInstrumentation( final Instrumentation instrumentation) {
			DynamicAspectJWeaverAgent.instrumentation = instrumentation;
		}

	// Regarding name of options given to agentmain method ----------------------------------------
	/**
	 * For parsing argument given to {@link DynamicAspectJWeaverAgent#agentmain(String, Instrumentation)} 
	 * method and storing argument value.
	 * 
	 * @author <a href="mailto:artymt@gmail.com">Arata Y.</a>
	 */
	public static enum Option {
		/**
		 * For argument of path to aspectjweaver.jar file that 
		 * {@link DynamicAspectJWeaverAgent#agentmain(String, Instrumentation)} method  
		 * adds it to application class path dynamically. 
		 * 
		 * 
		 * if <code>agentmain</code> method finds that 
		 * 
		 * 
		 * it cannot be loaded. 
		 */
		AspectjWeaverJar( "aspectjweaverjar", String.class),
		/**
		 * For argument going to given to premain method of agent class in .jar file specified by 
		 * the argument value of {@link Option#AspectjWeaverJar}.
		 */
		AspectjWeaverAgentMethodArg( "agent_method_arg", String.class),
		/**
		 * For argument of paths to .jar files what will be added to application class path 
		 * (before enabling LTW) in {@link #agentmain(String, Instrumentation)} method. 
		 * For multiple paths, each path should have been separated by {@link File#pathSeparator}. 
		 */
		AdditionalJarPaths( "jar_adition", (new String[]{}).getClass()), 
		/**
		 * For argument of switch whether checking jar file contains in the argument value of 
		 * {@link Option#AdditionalJarPaths} has been already loaded before adding to 
		 * application system class path.
		 */
		CheckJarAddition( "checkjaraddition", Boolean.class);
		
		private final String argName;
			public String getArgName() {
				return argName;
			}
		private final Class<?> argValueType;
			public Class<?> getArgValueType() {
				return argValueType;
			}
		private Option( final String argName, final Class<?> argValueType) {
			this.argName = argName;
			this.argValueType = argValueType;
		}
	}
	
	private Map<Option, Object> optionMap = new HashMap<DynamicAspectJWeaverAgent.Option, Object>();
		synchronized void setOptionValue( final Option option, final Object value) {
			if ( option == null) {
				throw new IllegalArgumentException( "value of option input cannot be null.");
			}
			
			if ( value != null) {
				if ( !value.getClass().equals( option.getArgValueType())) {
					throw new IllegalArgumentException( 
							String.format(
									"value %1$s is not appropriate to be stored for %2$s " 
									+ "because its type (%3$s) is not expected type, %4$s.",
									value.toString(), 
									option.name(), 
									value.getClass().getName(),
									option.getArgValueType().getName()
									)
							);
				}
			}
			optionMap.put( option, value);
		}
		synchronized Object getOptionValue( final Option option) {
			if ( option == null) {
				throw new IllegalArgumentException( "value of option input cannot be null.");
			}
			
			Object argValue = optionMap.get( option);
			if ( option.getArgValueType().equals( (new String[]{}).getClass())) {
				if ( argValue != null) {
					return ((String[])argValue).clone();
				}
			}
			else if ( Option.CheckJarAddition.equals( option)) {
				if ( argValue == null) {
					argValue = Boolean.TRUE;
					setOptionValue( option, argValue);
				}
			}
			return argValue;
		}
	// --------------------------------------------------------------------------------------------
	
	protected void logginUnnecessityOfWeaverAgentLoad( 
			Class<?> aspectjAgentClass, String aspectjWeaverJarPath) 
	{
		Logger logger = getLogger();
		if ( logger.isLoggable( Level.WARNING)) {
			File aspectJWeaverAgentFile = null;
				try {
					aspectJWeaverAgentFile = new File( 
							aspectjAgentClass.getProtectionDomain().getCodeSource()
							.getLocation().toURI());
				}
				catch( Exception exception) { // Ignore
				}
			logger.log(
					Level.WARNING,
					String.format(
							"Proceeding without adding %1$s to application class " 
							+ "path since %2$s has already been loaded from %3$s. " 
							+ "If class loading problem occurs later as you may " 
							+ "expect because of having skipped adding it, then please " 
							+ "add it manually via Instrumentation instance " 
							+ "accessible by getInstrumentation method.",
							aspectjWeaverJarPath, 
							aspectjAgentClass.getName(), 
							((aspectJWeaverAgentFile == null) 
									? "archival place" 
										: aspectJWeaverAgentFile.getAbsolutePath())
							)
					);
		}
	}
	
	/**
	 * parse arguments given to {@link #agentmain(String, Instrumentation)} method.
	 * When value of <code>options</code> input is null, do nothing.
	 * @param options
	 */
	protected synchronized void parseOptions( final String options) {
		Logger logger = getLogger();
			if ( logger.isLoggable( Level.FINER)) {
				StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[ 1];
				logger.entering( 
						stackTraceElement.getClassName(), 
						stackTraceElement.getMethodName(),
						new Object[]{ options}
						);
			}
		if ( options != null) {
			String optionsCopy = options.trim();
				// Eliminate double-quotation characters at start and end of options input    
				if ( options.matches( "^\"(.)*\"$")) {
					optionsCopy = optionsCopy.replaceAll( "^\"", "").replaceAll( "\"$", "");
				}
			boolean agentMethodArgProcessingSign = false;
				// Switch to indicate if argument processing for Option.AspectjWeaverAgentMethodArg is 
				// terminated or not. This boolean variable is for the case that argument to agent method 
				// contains ',' character.
			boolean agentMethodArgIncident = false; 
				// this is just to control logging for Option.AspectjWeaverAgentMethodArg
			for( String optionStr : optionsCopy.split( ",")) {
				String optionStrCopy = optionStr.trim();
					// Eliminate double-quotation characters at start and end    
					if ( optionStrCopy.matches( "^\"(.)*\"$")) {
						optionStrCopy = optionStrCopy.replaceAll( "^\"", "").replaceAll( "\"$", "");
					}
				Option optionObj;
				if ( optionStrCopy.matches( "^" + Option.AspectjWeaverJar.getArgName() + " *=.+")) {
					agentMethodArgProcessingSign = false;
					optionObj = Option.AspectjWeaverJar;
					String[] optionTokenArray 
					= optionStrCopy.split( "^" + optionObj.getArgName() + " *= *");
					if ( optionTokenArray.length > 1) {
						setOptionValue( optionObj, optionTokenArray[ 1].replaceAll( "\"", "").trim());
							if ( logger.isLoggable( Level.FINE)) {
								logger.log( Level.FINE, 
										String.format(
												"Detected \"%1$s\" for %2$s argument.",
												getOptionValue( optionObj), optionObj.getArgName()
												));
							}
					}
				}
				else if ( optionStrCopy.matches( 
						"^" + Option.AspectjWeaverAgentMethodArg.getArgName() + " *=.+")) {
					agentMethodArgProcessingSign = true;	agentMethodArgIncident = true;					
					optionObj = Option.AspectjWeaverAgentMethodArg;
					String[] optionTokenArray 
					= optionStrCopy.split( "^" + optionObj.getArgName() + " *= *");
					if ( optionTokenArray.length > 1) {
						setOptionValue( optionObj, optionTokenArray[ 1].trim());
					}
					// case of that ',' character is contained in argument to agent method will be 
					// handled later else block.
				}
				else if ( optionStrCopy.matches( "^" + Option.AdditionalJarPaths.getArgName() + " *=.+")) {
					agentMethodArgProcessingSign = false;
					optionObj = Option.AdditionalJarPaths;
					String[] optionTokenArray = optionStrCopy.split( "^" + optionObj.getArgName() + " *= *");
					if ( optionTokenArray.length > 1) {
						String[] jarPathArray = optionTokenArray[ 1].split( File.pathSeparator);
							for( int jarPathIndex = 0; jarPathIndex < jarPathArray.length; jarPathIndex++) {
								jarPathArray[ jarPathIndex] 
								= jarPathArray[ jarPathIndex].replaceAll( "\"", "").trim();
							} // for
						setOptionValue( optionObj, jarPathArray);
							if ( logger.isLoggable( Level.FINE)) {
								logger.log( Level.FINE, 
										String.format(
												"Detected \"%1$s\" for %2$s argument.",
												Arrays.toString( jarPathArray), optionObj.getArgName()
												));
							}
					}
				}
				else if ( optionStrCopy.matches( "^" + Option.CheckJarAddition.getArgName() + " *=.+")) {
					agentMethodArgProcessingSign = false;
					optionObj = Option.CheckJarAddition;
					String[] optionTokenArray = optionStrCopy.split( "^" + optionObj.getArgName() + " *= *");
					boolean argValue = true;
					if ( optionTokenArray.length > 1) {
						if ( optionTokenArray[ 1] != null) {
							if ( optionTokenArray[ 1].trim().toLowerCase()
									.matches( "^\"?((false)|(no)|(off))\"?$")) {
								argValue = false;
							}
						}
					}
					setOptionValue( optionObj, Boolean.valueOf( argValue));
						if ( logger.isLoggable( Level.FINE)) {
							logger.log( Level.FINE, 
									String.format(
											"Detected \"%1$b\" for %2$s argument.",
											argValue, optionObj.getArgName()
											));
						}
				}
				else { // the case that argument to agent method contains ',' character.
					if ( agentMethodArgProcessingSign) {
						optionObj = Option.AspectjWeaverAgentMethodArg;
						String argStockForAgentMethodArg = (String)(getOptionValue( optionObj));
							if ( argStockForAgentMethodArg == null) {
								argStockForAgentMethodArg = "";
							}
						if ( argStockForAgentMethodArg.length() < 1) {
							setOptionValue( optionObj, optionStr.trim());
						}
						else {
							setOptionValue( optionObj, argStockForAgentMethodArg + "," + optionStr.trim());
						}
					}
				}
			} // for
			
			if ( agentMethodArgIncident) {
				if ( logger.isLoggable( Level.FINE)) {
					logger.log( Level.FINE, 
							String.format(
									"Detected \"%1$s\" for %2$s argument.",
									getOptionValue( Option.AspectjWeaverAgentMethodArg), 
									Option.AspectjWeaverAgentMethodArg.getArgName()
									));
				}
			}
		}
		
		if ( logger.isLoggable( Level.FINER)) {
			StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[ 1];
			logger.exiting( 
					stackTraceElement.getClassName(), 
					stackTraceElement.getMethodName()
					);
		}
	}
	
	public static enum AgentType {
		Agentmain( "Agent-Class", "agentmain"), Premain( "Premain-Class", "premain");
		
		private final String manifestAttributeName;
			public String getManifestAttributeName() {
				return manifestAttributeName;
			}
		private final String agentMethodName;
			public String getAgentMethodName() {
				return agentMethodName;
			}

		private AgentType( String manifestAttributeName, String agentMethodName) {
			this.manifestAttributeName = manifestAttributeName;
			this.agentMethodName = agentMethodName;
		}
	}
	
	static class AgentInfo {
		private JarFile agentJarFile;
			JarFile getAgentJarFile() {
				return agentJarFile;
			}
			void setAgentJarFile( final JarFile agentJarFile) {
				this.agentJarFile = agentJarFile;
			}
		private AgentType agentType;
			AgentType getAgentType() {
				return agentType;
			}
			void setAgentType( final AgentType agentType) {
				this.agentType = agentType;
			}
		private String agentClassName;
			String getAgentClassName() {
				return agentClassName;
			}
			void setAgentClassName( final String agentClassName) {
				this.agentClassName = agentClassName;
			}
		private Class<?> agentClass;
			Class<?> getAgentClass() {
				return agentClass;
			}
			void setAgentClass( final Class<?> agentClass) {
				this.agentClass = agentClass;
			}
	}
	
	/**
	 * Extract class name having set for either Agent-Class or Premain-Class attribute in 
	 * MANIFEST.MF of given <code>jarFile</code> input. <br />
	 * Agent-Class attribute has precedence over Premain-Class attribute when both attribute entries 
	 * are found. <br />
	 * If neither of attributes is found, then null will be returned.
	 * @param jarFile
	 * @return AgentInfo object having set member fiels except {@link AgentInfo#agentClass}, or null.
	 */
	AgentInfo extractAgentInfo( final JarFile jarFile) {
		if ( jarFile == null) {
			throw new IllegalArgumentException( "Value of jarFile cannot be null.");
		}
		
		AgentInfo agentInfo = new AgentInfo();
			agentInfo.setAgentJarFile( jarFile);
		try {
			for( 
					AgentType agentType = AgentType.Agentmain;
					agentInfo.getAgentClassName() == null;
					agentType = AgentType.Premain
					) 
			{
				agentInfo.setAgentType( agentType);
				agentInfo.setAgentClassName(
						jarFile.getManifest()
							.getMainAttributes().getValue( agentType.getManifestAttributeName())
						);
				if ( agentType.equals( AgentType.Premain)) {
					break; // for
				}
			} // for
		}
		catch( Exception exception) {
			throw new IllegalArgumentException(
					String.format(
							"Failure in accessing MANIFEST.MF in %1$s (given by %2$s argument)",
							jarFile.getName(), 
							Option.AspectjWeaverJar.getArgName()
							),
					exception);
		}
		
		if ( agentInfo.getAgentClassName() == null) agentInfo = null;
		return agentInfo;
	}
	
	File locateAspectJWeaverJar() {
		// Try to get path to aspectjweaver.jar from system property
		File aspectJWeaverArchive = null;
		
		String aspectjHomePath = System.getenv( "ASPECTJ_HOME");
		if ( aspectjHomePath != null) {
			File aspectjHomeDir = new File( aspectjHomePath);
			if ( aspectjHomeDir.exists() && aspectjHomeDir.isDirectory()) {
				if( "bin".equals( aspectjHomeDir.getName())) {
					aspectjHomeDir = aspectjHomeDir.getParentFile();
				}
				File aspectjLibDir = new File( aspectjHomeDir, "lib");
				if ( aspectjHomeDir.exists() && aspectjHomeDir.isDirectory()) {
					FileFilter fileFilter = new FileFilter() {
						@Override
						public boolean accept( File file) {
							if ( !file.isFile()) return false;
							//ex. aspectjweaver-1.7.0.jar
							String fileName = file.getName().toLowerCase();
								if ( !fileName.endsWith( ".jar")) return false;
								if ( fileName.startsWith( "aspectjweaver")) return true;
							return false;
						}
					};
					File[] fileArray = aspectjLibDir.listFiles( fileFilter);
						if ( fileArray.length == 1) {
							aspectJWeaverArchive = fileArray[ 0];
						}
						else {
							Logger logger = getLogger();
							if ( logger.isLoggable( Level.FINE)) {
								logger.log( 
										Level.FINE, 
										String.format(
												"Could not determine AspectJ Weaver .jar file in " 
												+ "%1$s directory: %2$s",
												aspectjLibDir.getAbsolutePath(),
												(( fileArray.length == 0) 
													? "no candidate" : Arrays.toString( fileArray))
												)
										);
							}
						}
						
				}
			}
		}
		if ( aspectJWeaverArchive == null) {
			throw new UnsupportedOperationException(
					String.format(
							"Could not load %1$s class. To enable AspectJ's load time " 
							+ "weaving, either start application with pointing " 
							+ "aspectjweaver.jar with -javaagent JVM parameter, use " 
							+ "custom class loader (such as WeavingURLClassLoader), or use " 
							+ "AspectJWeaverAgentLoader and %2$s with giving either path to " 
							+ "aspectjweaver.jar or setting ASPECTJ_HOME system environment " 
							+ "variable pointing AspectJ installation directory.",
							DynamicAspectJWeaverAgent.getAspectjWeaverAgentClassName(), 
							DynamicAspectJWeaverAgent.class.getSimpleName()
							)
					);
		}
		
		return aspectJWeaverArchive;
	}
	
	/**
	 * Actual worker method called by {@link #agentmain(String, Instrumentation)} method. <br />
	 * This is to enable AspectJ's LTW (load time weaving) dynamically. <br />
	 * In this method, the execution process progresses like:
	 * <ol>
	 * <li>Add AspectJ's agent .jar file specified by argument in <code>options</code> input to 
	 * application's system class path, if it has not been loaded. The name of argument in 
	 * <code>options</code> input is identified by <code>AspectjWeaverJar.getArgName</code> method of 
	 * {@link Option} enum.</li>
	 * <li>If AspectJ's agent .jar file is not given by the argument, add aspectjweaver.jar 
	 * at where ASPECTJ_HOME system environment variable points to application's system class path.</li>
	 * <li>Add additional .jar files given by the argument (of what name is identified 
	 * by <code>AdditionalJarPaths.getArgName</code> method of {@link Option} enum) in <code>options</code> 
	 * input to application's system class path.</li>
	 * <li>Call agentmain method of the subject agent if it can be found. <br />
	 * If not, then call premain method instead.</li>
	 * </ol> 
	 * 
	 * @param options may specify the path to agent's .jar, paths of additional jar files, and  
	 * arguments to either agentmain or premain method of the subject's agent. <br />
	 * For name of arguments, see {@link Option}.
	 * @param instrumentation should be given automatically by Attach API component.
	 */
	protected synchronized void agentmainWorker( final String options, final Instrumentation instrumentation) {
		Logger logger = getLogger();
			if ( logger.isLoggable( Level.FINER)) {
				StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[ 1];
				logger.entering( 
						stackTraceElement.getClassName(), 
						stackTraceElement.getMethodName(),
						new Object[]{ options, instrumentation}
						);
			}
		
		if ( instrumentation == null) {
			throw new IllegalArgumentException( "Value of instrumentation input cannot be null.");
		}
		DynamicAspectJWeaverAgent.setInstrumentation( instrumentation);
		
		// parsing options 
		parseOptions( options);
		
		// Add agent jar to application class path if it has not been loaded yet ------------------
		AgentInfo agentInfo = null;
		
		if ( getOptionValue( Option.AspectjWeaverJar) != null) {
			String aspectjWeaverJarPath = (String)(getOptionValue( Option.AspectjWeaverJar));
			File aspectjWeaverFile = new File( aspectjWeaverJarPath);
			JarFile aspectjWeaverJarFile = null;
				try {
					aspectjWeaverJarFile = new JarFile( aspectjWeaverFile);
				}
				catch( Exception exception) {
					throw new IllegalArgumentException(
							String.format(
									"Failure in handling %1$s (given by %2$s argument) as an .jar file.",
									aspectjWeaverJarPath, 
									Option.AspectjWeaverJar.getArgName()
									),
							exception);
				}
			
			agentInfo = extractAgentInfo( aspectjWeaverJarFile);	
				if ((agentInfo != null) && (agentInfo.getAgentClassName() != null)) {
					// Check whether Agent has been already loaded by system class loader
					AgentAssistant agentAssistant = new AgentAssistant();
					agentInfo.setAgentClass( 
							agentAssistant.hasLoaded( agentInfo.getAgentClassName(), instrumentation));
					if ( agentInfo.getAgentClass() != null) {
						logginUnnecessityOfWeaverAgentLoad( 
								agentInfo.getAgentClass(), aspectjWeaverJarPath);
					}
					else {
						// Check whether Agent has been already in application class path
						ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
						try {
							agentInfo.setAgentClass( 
									systemClassLoader.loadClass( agentInfo.getAgentClassName()));
						}
						catch( ClassNotFoundException exception) { // ignore
						}
						if ( agentInfo.getAgentClass() != null) {
							logginUnnecessityOfWeaverAgentLoad( 
									agentInfo.getAgentClass(), aspectjWeaverJarPath);
						}
						else {
							// add agent jar to application's system class path
							instrumentation.appendToSystemClassLoaderSearch( aspectjWeaverJarFile);
								if ( logger.isLoggable( Level.FINE)) {
									logger.log(
											Level.FINE,
											String.format(
													"Succeeded to add %1$s to class path of application.",
													aspectjWeaverJarPath
													)
											);
							}
								
							try {
								agentInfo.setAgentClass(
										systemClassLoader.loadClass( agentInfo.getAgentClassName()));
							}
							catch ( Exception exception) {
								throw new RuntimeException( 
										String.format(
												"Failure in loading %1$s by system class loader after " 
												+ "adding %2$s to application's system class path.",
												agentInfo.getAgentClassName(), 
												aspectjWeaverJarPath
												), 
										exception);
							}
						}
					}
				}
				else {
					throw new IllegalArgumentException( 
							String.format(
									"Cannot find Agent-Class or Premain-Class entry in main attribute " 
									+ "seciton of MANIFEST.MF file in %1$s", 
									aspectjWeaverJarPath)
							);
				}
		}
		else {
			// Check whether aspectjweaver agent has been loaded
			boolean hasAspectjWeaverLoaded = false;
				agentInfo = new AgentInfo();
					agentInfo.setAgentClassName( 
							DynamicAspectJWeaverAgent.getAspectjWeaverAgentClassName());
					AgentAssistant agentAssistant = new AgentAssistant();
					agentInfo.setAgentClass(
							agentAssistant.hasLoaded( agentInfo.getAgentClassName(), instrumentation));
				if ( agentInfo.getAgentClass() != null) {
					hasAspectjWeaverLoaded = true;
				}
				else {
					// Check whether Agent has been already in application class path
					ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
					try {
						agentInfo.setAgentClass(
								systemClassLoader.loadClass( agentInfo.getAgentClassName()));
					}
					catch( ClassNotFoundException exception) { // Ignore
					}
					if ( agentInfo.getAgentClass() != null) {
						hasAspectjWeaverLoaded = true;
					}
				}
			
			// If not, then try to load from where ASPECTJ_HOME system environment variable points
			if ( !hasAspectjWeaverLoaded) {
				File aspectJWeaverArchiveFile = locateAspectJWeaverJar();
				
				// add aspectjweaver.jar to application's system class path
				JarFile aspectjWeaverJarFile = null;
				try {
					aspectjWeaverJarFile = new JarFile( aspectJWeaverArchiveFile);
					instrumentation.appendToSystemClassLoaderSearch( aspectjWeaverJarFile);
						if ( logger.isLoggable( Level.FINE)) {
							logger.log(
									Level.FINE,
									String.format(
											"Succeeded to add %1$s to class path of application.",
											aspectJWeaverArchiveFile
											)
									);
					}
				}
				catch( Exception exception) {
					if ( exception instanceof RuntimeException) throw (RuntimeException)exception;
					throw new RuntimeException(
							String.format(
									"Failed adding %1$s to class path of application " 
									+ "in enabling AspectJ's load time weaving.",
									aspectJWeaverArchiveFile
									),
							exception);
				}
				
				agentInfo = extractAgentInfo( aspectjWeaverJarFile);
					if ((agentInfo != null) && (agentInfo.getAgentClassName() != null)) {
						ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
						try {
							agentInfo.setAgentClass(
									systemClassLoader.loadClass( agentInfo.getAgentClassName()));
						}
						catch ( Exception exception) {
							throw new RuntimeException( 
									String.format(
											"Failure in loading %1$s by system class loader after " 
											+ "adding %2$s to application's system class path.",
											agentInfo.getAgentClassName(), 
											aspectJWeaverArchiveFile
											), 
									exception);
						}
					}
					else {
						throw new IllegalArgumentException( 
								String.format(
										"Cannot find Agent-Class or Premain-Class entry in main attribute " 
										+ "seciton of MANIFEST.MF file in %1$s", 
										aspectjWeaverJarFile.getName())
								);
					}
			}
		}
		// ----------------------------------------------------------------------------------------
		
		// Add additional jars to application class path ------------------------------------------
		if ( getOptionValue( Option.AdditionalJarPaths) != null) {
			boolean checkJarAddition = true;
				if ( getOptionValue( Option.CheckJarAddition) != null) {
					checkJarAddition 
					= ((Boolean)(getOptionValue( Option.CheckJarAddition))).booleanValue();
				}
			AgentAssistant agentAssistant = new AgentAssistant();
			agentAssistant.addAdditionalJarPathsToSystemClassPath(
					(String[])(getOptionValue( Option.AdditionalJarPaths)),
					checkJarAddition, 
					instrumentation
					);
		}
		// ----------------------------------------------------------------------------------------
	
		Instrumentation superInstrumentationObj = null;
			try {
				Method getInstrumentationMethod 
				= agentInfo.getAgentClass().getMethod( "getInstrumentation", (Class<?>[])null);
					getInstrumentationMethod.setAccessible( true);
				superInstrumentationObj 
				= (Instrumentation)getInstrumentationMethod.invoke( null, (Object[])null);
			}
			catch( Exception exception) { // Do nothing
			}
			
		if ( superInstrumentationObj != null) {
			if ( logger.isLoggable( Level.INFO)) {
				logger.log(
						Level.INFO,
						String.format(
								"%1$s JavaAgent has already been enabled.", 
								agentInfo.getAgentClass().getSimpleName()
								)
						);
			}
		}
		else {
			Method agentMethod = null;
				try {
					agentMethod 
					= agentInfo.getAgentClass().getMethod( 
							agentInfo.getAgentType().getAgentMethodName(), 
							String.class, Instrumentation.class);
				}
				catch( Exception exception) {
					throw new RuntimeException(
							String.format(
									"Could not find %1$s static method in %2$s.",
									agentInfo.getAgentType().getAgentMethodName(), 
									agentInfo.getAgentClass().getName()
									),
							exception
							);
				}
				agentMethod.setAccessible( true);
				
			try {
				agentMethod.invoke( 
						null, getOptionValue( Option.AspectjWeaverAgentMethodArg), instrumentation);
					if ( logger.isLoggable( Level.FINE)) {
						logger.log(
								Level.FINE,
								String.format(
										"Succeded to call %1$s static method of %2$s JavaAgent to " 
										+ "dynamically enable it.",
										agentMethod.getName(),
										agentInfo.getAgentClass().getSimpleName()
										)
								);
					}
			}
			catch( Exception exception) {
				if ( exception instanceof RuntimeException) throw (RuntimeException)exception;
				
				String agentArchivePath = "";
					try {
						File agentArchiveFile 
						= new File( agentInfo.getAgentClass().getProtectionDomain().getCodeSource()
								.getLocation().toURI());
						agentArchivePath 
						= String.format( 
								"from %1$s ",
								agentArchiveFile.getAbsolutePath()
								);
					}
					catch( Exception ignored) { // ignore
					}
				throw new RuntimeException(
						String.format(
								"Failure in invoking %1$s static method of %2$s class %3$s" 
								+ "in order to dynamically enable it.",
								agentMethod.getName(),
								agentInfo.getAgentClass().getName(), 
								agentArchivePath
								),
								exception
						);
			}
		}
		
		if ( logger.isLoggable( Level.FINER)) {
			StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[ 1];
			logger.exiting( 
					stackTraceElement.getClassName(), 
					stackTraceElement.getMethodName()
					);
		}
	}
	
	/**
	 * Java agent method wrapping {@link #agentmainWorker(String, Instrumentation)} method. 
	 * 
	 * @param options will be handed over to <code>agentmainWorker</code> method.
	 * @param instrumentation will be handed over to <code>agentmainWorker</code> method.
	 * @see #agentmainWorker(String, Instrumentation)
	 */
	public synchronized static void agentmain( final String options, final Instrumentation instrumentation) {
		(new DynamicAspectJWeaverAgent()).agentmainWorker( options, instrumentation);
	}
}